/**
 * Software License, Version 1.0
 * 
 * Copyright 2008 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgment:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgment may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or Granules, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called Granules, 
 * nor may Indiana University or Community Grids Lab or Granules appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 * LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 * WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 * NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 * GENERATED USING SOFTWARE.
 */
package cgl.granules.util;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Random;

/**
 * A class that encapsulates a Matrix. Utility methods are provided to
 * read/write matrices that have elements of different data types.
 * 
 * @author Shrideep Pallickara
 * 
 */
public class Matrix {
    public static final int INT_ENTRIES = 1;
    public static final int LONG_ENTRIES = 2;

    private String filename = "";
    private FileChannel fileChannel = null;
    private long[][] matrix;
    private int META_DATA_LENGTH = 16;
    private int SHORT_BYTES = 2;
    private int INT_BYTES = 4;
    private int LONG_BYTES = 8;
    private int FLOAT_BYTES = 4;
    private int DOUBLE_BYTES = 8;
    private int tracker = 0;

    private int numOfRows;
    private int numOfColumns;
    private boolean rowMajorFormat = true;
    private int entriesType;
    private String moduleName = "Matrix: ";


    public Matrix(String filename, int numberOfRows, int numberOfColumns,
            boolean rowFormat, int entriesType) throws IOException {

        RandomAccessFile raf = new RandomAccessFile(filename, "rw");
        fileChannel = raf.getChannel();

        if (fileChannel.size() > META_DATA_LENGTH) {
            this.filename = filename;
            readMetadata();

            if (numOfRows != numberOfRows || numOfColumns != numberOfColumns) {
                String errorReport =
                    moduleName + "For the matrix in file [" + filename
                        + "] there is a mismatch in the specified ["
                        + numberOfRows + "," + numberOfColumns + "] dimensions"
                        + " & the metadata associated with the matrix = ["
                        + numOfRows + "," + numOfColumns + "]";
                throw new IOException(errorReport);
            }

            if (rowMajorFormat != rowFormat) {
                String specifiedFormat = "Row Major Format";
                String metadataFormat = "Row Major Format";
                if (!rowMajorFormat) {
                    metadataFormat = "Column Major Format";
                }
                if (!rowFormat) {
                    specifiedFormat = "Column Major Format";
                }
                throw new IOException(moduleName + "in file (" + filename
                    + ") has Requested [" + specifiedFormat
                    + "], but has been stored in [" + metadataFormat + "]");
            }

            printMetadata();
            return;
        }

        this.filename = filename;
        this.numOfRows = numberOfRows;
        this.numOfColumns = numberOfColumns;
        this.rowMajorFormat = rowFormat;
        this.entriesType = entriesType;

        writeMetadata();
    }


    public Matrix(String filename) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(filename, "rw");
        fileChannel = raf.getChannel();

        if (fileChannel.size() < META_DATA_LENGTH) {
            String errorReport =
                "The matrix in file (" + filename
                    + ") has NOT Been initialized properly!";
            throw new IOException(errorReport);
        }
        readMetadata();
        printMetadata();
    }


    public long size() throws IOException {
        return fileChannel.size();
    }


    public int getMetadataLength() {
        return META_DATA_LENGTH;
    }


    private void printMetadata() {
        String storageType = "Row Major Format";
        if (!rowMajorFormat) {
            storageType = "Column Major Format";
        }
        String info =
            moduleName + "The matrix stored in file (" + filename
                + ") has dimensions [" + numOfRows + "," + numOfColumns
                + "], has been stored in " + storageType
                + ", and the entries are of type (" + getEntriesTypeString()
                + ")";
        System.out.println(info);
    }


    private String getEntriesTypeString() {

        switch (entriesType) {
        case INT_ENTRIES:
            return "int";
        case LONG_ENTRIES:
            return "long";
        default:
            return "unknown type";
        }

    }


    private synchronized void readMetadata() throws IOException {
        ByteBuffer metadataBuffer = ByteBuffer.allocate(META_DATA_LENGTH);

        fileChannel.position(0);
        fileChannel.read(metadataBuffer);

        metadataBuffer.flip();

        numOfRows = metadataBuffer.getInt();
        numOfColumns = metadataBuffer.getInt();

        int rowMajorIndicator = metadataBuffer.getInt();
        if (rowMajorIndicator == 1) {
            rowMajorFormat = true;
        } else {
            rowMajorFormat = false;
        }

        entriesType = metadataBuffer.getInt();
    }


    private synchronized void writeMetadata() throws IOException {
        ByteBuffer metadataBuffer = ByteBuffer.allocate(META_DATA_LENGTH);

        metadataBuffer.putInt(numOfRows);
        metadataBuffer.putInt(numOfColumns);
        if (rowMajorFormat) {
            metadataBuffer.putInt(1);
        } else {
            metadataBuffer.putInt(0);
        }
        metadataBuffer.putInt(entriesType);

        metadataBuffer.flip();

        fileChannel.position(0);
        fileChannel.write(metadataBuffer);

    }


    public boolean isRowMajorFormat() {
        return rowMajorFormat;
    }


    public boolean isColumnMajorFormat() {
        return !rowMajorFormat;
    }


    public int getNumberOfRows() {
        return numOfRows;
    }


    public int getNumberOfColumns() {
        return numOfColumns;
    }


    public int getEntriesType() {
        return entriesType;
    }


    /**
     * This method reads the specified row of a matrix that has been written to
     * a file. This method also requires the number of columns to be specified.
     * 
     * @param rowId
     *            The matrix row that needs to be retrieved
     * @throws IOException
     *             If there are problems locating the file or reading its
     *             contents.
     */
    public synchronized long[] readLongMatrixRow(int rowId) throws IOException {

        long position = (rowId * numOfColumns) * LONG_BYTES + META_DATA_LENGTH;

        ByteBuffer rowBuffer = ByteBuffer.allocate(numOfColumns * LONG_BYTES);
        rowBuffer.clear();
        fileChannel.position(position);
        fileChannel.read(rowBuffer);
        rowBuffer.flip();

        long[] row = new long[numOfColumns];

        for (int i = 0; i < numOfColumns; i++) {
            row[i] = rowBuffer.getLong();
        }
        return row;
    }


    /**
     * This method reads a specified column from the matrix that has been
     * written to a file. This method also requires the number of matrix rows to
     * be specified.
     * 
     * @param columnId
     *            The matrix column that needs to be retrieved
     * @return The column[<columnId>] in question
     * @throws IOException
     *             If there are problems locating the file or reading its
     *             contents.
     */
    public synchronized long[] readLongMatrixColumn(int columnId)
        throws IOException {

        long[] column = new long[numOfRows];
        ByteBuffer workingBuffer = ByteBuffer.allocate(LONG_BYTES);

        for (int i = 0; i < numOfRows; i++) {
            long position =
                (((numOfColumns * i) + columnId) * LONG_BYTES)
                    + META_DATA_LENGTH;
            column[i] = getLongData(fileChannel, position, workingBuffer);
        }

        return column;
    }


    /**
     * Reads a matrix element from the underlying file based on the specified
     * <code>[rowId, columId]</code>.
     * 
     * @param rowId
     *            The rowId for the matrix element
     * @param columnId
     *            The columnId for the matrix element
     * @return the value of the <code>matrix[rowId, columnId]</code> element.
     * @throws IOException
     *             If there are problems reading the file.
     */
    public synchronized long readLongMatrixElement(int rowId, int columnId)
        throws IOException {

        ByteBuffer elementBuffer = ByteBuffer.allocate(LONG_BYTES);
        long position =
            (((numOfColumns * rowId) + columnId) * LONG_BYTES)
                + META_DATA_LENGTH;

        fileChannel.position(position);
        fileChannel.read(elementBuffer);
        elementBuffer.flip();

        return elementBuffer.getLong();
    }


    /**
     * Read a matrix that has been written to a file (specified in the
     * constructor) based on the specified rows and columns. Depending on the
     * size of the matrix, trying to read the entire matrix can lead to
     * {@link OutOfMemoryError}
     * 
     * @return The long[][] matrix
     * @throws IOException
     *             If there are problems reading from the file.
     */
    public synchronized long[][] readLongMatrix() throws IOException {

        long[][] fileMatrix = new long[numOfRows][numOfColumns];

        for (int i = 0; i < numOfRows; i++) {
            fileMatrix[i] = readLongMatrixRow(i);
        }

        return fileMatrix;
    }


    /**
     * Write a specific matrix element code>[rowId, columnId]</code> to the
     * file.
     * 
     * @param rowId
     *            The row identifier for the element
     * @param columnId
     *            The column identifier for the element
     * @param value
     *            The value of the matrix element <code>[rowId, columnId]</code>
     * @throws IOException
     *             If there are problems writing to the file.
     */
    public synchronized void writeLongMatrixElement(int rowId, int columnId,
                                                    long value)
        throws IOException {

        ByteBuffer elementBuffer = ByteBuffer.allocate(LONG_BYTES);
        long position =
            (((rowId * numOfColumns) + columnId) * LONG_BYTES)
                + META_DATA_LENGTH;

        if (!rowMajorFormat) {
            position =
                (((numOfRows * columnId) + rowId) * LONG_BYTES)
                    + META_DATA_LENGTH;
        }

        elementBuffer.putLong(value);
        elementBuffer.flip();

        fileChannel.position(position);
        fileChannel.write(elementBuffer);
    }


    /**
     * Write a row of the matrix to the underlying file.
     * 
     * @param rowId
     *            The identifier of the matrix row
     * @param rowValues
     *            The values associated with the row
     * @throws IOException
     *             If there are problems writing to the file.
     */
    public synchronized void writeLongMatrixRow(int rowId, long[] rowValues)
        throws IOException {

        int numOfColumns = rowValues.length;

        long position =
            (((rowId * numOfColumns)) * LONG_BYTES) + META_DATA_LENGTH;

        ByteBuffer rowBuffer = ByteBuffer.allocate(numOfColumns * LONG_BYTES);
        for (int i = 0; i < numOfColumns; i++) {
            rowBuffer.putLong(rowValues[i]);
        }
        rowBuffer.flip();
        fileChannel.position(position);
        fileChannel.write(rowBuffer);
    }


    /**
     * Write a column of the matrix to the underlying file.
     * 
     * @param columnId
     *            The columnId for the matrix column
     * @param columnValues
     *            Values associated with the column
     * @throws IOException
     *             If there are problems writing to the file.
     */
    public synchronized void writeLongMatrixColumn(int columnId,
                                                   long[] columnValues)
        throws IOException {

        int numOfRows = columnValues.length;
        ByteBuffer elementBuffer = ByteBuffer.allocate(LONG_BYTES);
        long position = 0;
        for (int i = 0; i < numOfRows; i++) {
            position =
                (((i * numOfColumns) + columnId) * LONG_BYTES)
                    + META_DATA_LENGTH;
            elementBuffer.clear();
            elementBuffer.putLong(columnValues[i]);
            elementBuffer.flip();

            fileChannel.position(position);
            fileChannel.write(elementBuffer);
        }
    }


    /**
     * Retrieve a long from the underlying file at the specified position.
     */
    private long getLongData(FileChannel fileChannel, long position,
                             ByteBuffer byteBuffer) throws IOException {
        byteBuffer.clear();
        fileChannel.position(position);
        fileChannel.read(byteBuffer);
        byteBuffer.flip();
        long value = byteBuffer.getLong();
        return value;
    }


    /**
     * This method reads the specified row of a matrix that has been written to
     * a file. This method also requires the number of columns to be specified.
     * 
     * 
     * @param rowId
     *            The matrix row that needs to be retrieved
     * @throws IOException
     *             If there are problems locating the file or reading its
     *             contents.
     */
    public synchronized int[] readIntMatrixRow(int rowId) throws IOException {
        long position = (rowId * numOfColumns) * INT_BYTES + META_DATA_LENGTH;

        ByteBuffer rowBuffer = ByteBuffer.allocate(numOfColumns * INT_BYTES);
        rowBuffer.clear();
        fileChannel.position(position);
        fileChannel.read(rowBuffer);
        rowBuffer.flip();

        int[] row = new int[numOfColumns];

        for (int i = 0; i < numOfColumns; i++) {
            row[i] = rowBuffer.getInt();
        }
        return row;
    }


    /**
     * This method reads a specified column from the matrix that has been
     * written to a file. This method also requires the number of matrix rows to
     * be specified.
     * 
     * 
     * @param columnId
     *            The matrix column that needs to be retrieved
     * @return The column[<columnId>] in question
     * @throws IOException
     *             If there are problems locating the file or reading its
     *             contents.
     */
    public synchronized int[] readIntMatrixColumn(int columnId)
        throws IOException {

        int[] column = new int[numOfRows];
        ByteBuffer workingBuffer = ByteBuffer.allocate(INT_BYTES);

        for (int i = 0; i < numOfRows; i++) {
            long position =
                (((i * numOfColumns) + columnId) * INT_BYTES)
                    + META_DATA_LENGTH;
            column[i] = getIntData(position, workingBuffer);
        }

        return column;
    }


    /**
     * Reads a matrix element from the underlying file based on the specified
     * <code>[rowId, columId]</code>.
     * 
     * @param rowId
     *            The rowId for the matrix element
     * @param columnId
     *            The columnId for the matrix element
     * @return the value of the <code>matrix[rowId, columnId]</code> element.
     * @throws IOException
     *             If there are problems reading the file.
     */
    public synchronized int readIntMatrixElement(int rowId, int columnId)
        throws IOException {

        ByteBuffer elementBuffer = ByteBuffer.allocate(INT_BYTES);
        long position =
            (((numOfColumns * rowId) + columnId) * INT_BYTES)
                + META_DATA_LENGTH;
        fileChannel.position(position);
        fileChannel.read(elementBuffer);
        elementBuffer.flip();

        return elementBuffer.getInt();
    }


    /**
     * Read a matrix that has been written to a file (specified in the
     * constructor) based on the specified rows and columns. Depending on the
     * size of the matrix, trying to read the entire matrix can lead to
     * {@link OutOfMemoryError}
     * 
     * @return The matrix[][]
     * @throws IOException
     *             If there are problems reading from the file.
     */
    public synchronized int[][] readIntMatrix() throws IOException {

        int[][] fileMatrix = new int[numOfRows][numOfColumns];

        for (int i = 0; i < numOfRows; i++) {
            fileMatrix[i] = readIntMatrixRow(i);
        }

        return fileMatrix;
    }


    /**
     * Write a specific matrix element code>[rowId, columnId]</code> to the
     * file.
     * 
     * @param rowId
     *            The row identifier for the element
     * @param columnId
     *            The column identifier for the element
     * @param value
     *            The value of the matrix element <code>[rowId, columnId]</code>
     * @throws IOException
     *             If there are problems writing to the file.
     */
    public synchronized void writeIntMatrixElement(int rowId, int columnId,
                                                   int value)
        throws IOException {

        ByteBuffer elementBuffer = ByteBuffer.allocate(INT_BYTES);
        long position =
            (((rowId * numOfColumns) + columnId) * INT_BYTES)
                + META_DATA_LENGTH;
        elementBuffer.putInt(value);
        elementBuffer.flip();

        fileChannel.position(position);
        fileChannel.write(elementBuffer);
    }


    /**
     * Write a row of the matrix to the underlying file.
     * 
     * @param rowId
     *            The identifier of the matrix row
     * @param rowValues
     *            The values associated with the row
     * @throws IOException
     *             If there are problems writing to the file.
     */
    public synchronized void writeIntMatrixRow(int rowId, int[] rowValues)
        throws IOException {
        int numOfColumns = rowValues.length;

        long position =
            (((rowId * numOfColumns)) * INT_BYTES) + META_DATA_LENGTH;

        ByteBuffer rowBuffer = ByteBuffer.allocate(numOfColumns * INT_BYTES);

        for (int i = 0; i < numOfColumns; i++) {
            rowBuffer.putInt(rowValues[i]);
        }
        rowBuffer.flip();
        fileChannel.position(position);

        int bytesWritten = fileChannel.write(rowBuffer);
        int totalToBeWritten = numOfColumns * INT_BYTES;
        if (bytesWritten != totalToBeWritten) {
            System.out.println("Only (" + bytesWritten
                + ") bytes out of a possible (" + totalToBeWritten
                + " were written to disk!");
        }
    }


    /**
     * Write a column of the matrix to the underlying file.
     * 
     * @param columnId
     *            The columnId for the matrix column
     * @param columnValues
     *            Values associated with the column
     * @throws IOException
     *             If there are problems writing to the file.
     */
    public synchronized void writeIntMatrixColumn(int columnId,
                                                  int[] columnValues)
        throws IOException {
        int numOfRows = columnValues.length;
        ByteBuffer elementBuffer = ByteBuffer.allocate(INT_BYTES);
        long position = 0;
        for (int i = 0; i < numOfRows; i++) {
            position =
                (((i * numOfColumns) + columnId) * INT_BYTES)
                    + META_DATA_LENGTH;
            elementBuffer.clear();
            elementBuffer.putInt(columnValues[i]);
            elementBuffer.flip();

            fileChannel.position(position);
            fileChannel.write(elementBuffer);
        }
    }


    /**
     * Retrieve a long from the underlying file at the specified position.
     */
    private int getIntData(long position, ByteBuffer byteBuffer)
        throws IOException {
        byteBuffer.clear();
        fileChannel.position(position);

        if (fileChannel.read(byteBuffer) == -1) {
            long fileSize = fileChannel.size();
            String errorReport =
                "Encountered an END OF STREAM when reading " + "from file ("
                    + filename + ") of size =" + fileSize
                    + ".\n Trying to read at position =>" + position;
            throw new IOException(errorReport);
        }

        byteBuffer.flip();
        int value = byteBuffer.getInt();
        return value;
    }


    public int[] createDenseIntRow() {
        Random random = new Random();
        int[] rowValues = new int[numOfColumns];
        for (int i = 0; i < numOfColumns; i++) {
            rowValues[i] = random.nextInt();
        }
        return rowValues;
    }


    public int[] createDenseIntColumn(int numberOfRows) {
        Random random = new Random();
        int[] columnValues = new int[numberOfRows];
        for (int i = 0; i < numberOfRows; i++) {
            columnValues[i] = random.nextInt();
        }
        return columnValues;
    }


    /**
     * 
     * @param args
     */
    public static void main(String[] args) {
        int numOfRows = 16000;
        int numOfColumns = 16000;
        int entriesType = INT_ENTRIES;
        boolean rowMajorFormat = true;
        String filename = "MatrixA.txt";

        Random random = new Random();
        try {

            MatrixFactory matrixFactory = MatrixFactory.getInstance();

            Matrix matrixA =
                matrixFactory.getMatrix(filename, numOfRows, numOfColumns,
                                        entriesType, rowMajorFormat);

            long start = System.nanoTime();
            for (int i = 0; i < numOfRows; i++) {
                int rowAi[] = matrixA.createDenseIntRow();
                matrixA.writeIntMatrixRow(i, rowAi);
            }

            System.out.println("Time to write matrix[" + numOfRows + ","
                + numOfColumns + "] = "
                + ((System.nanoTime() - start) / 1000000) + " ms");

            start = System.nanoTime();
            for (int i = 0; i < 2000; i++) {
                System.out.print(i + " ");
                // int[] columnAi = matrixA.readIntMatrixColumn(numOfRows,
                // numOfColumns, i);
                int[] rowAi = matrixA.readIntMatrixRow(i);
            }
            System.out.println("Time to read matrix[" + numOfRows + ","
                + numOfColumns + "] = "
                + ((System.nanoTime() - start) / 1000000) + " ms");
            System.out.println("Test complete ...");

        } catch (IOException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }

    }
}
